--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit c93fe2ce0d07bbc0b8c046069e4f993fc7dd625c
Parents : d5630d7
Author : Mark Qvist <mark@unsigned.io>
Date : 2024-03-26T00:29:49+01:00
Added telemetry plugin functionality
Changes
5 files changed, 371 insertions(+), 47 deletions(-)
Diff
diff --git a/docs/example_plugins/telemetry.py b/docs/example_plugins/telemetry.py
new file mode 100644
index 00000000..97f90db8
--- /dev/null
+++ b/docs/example_plugins/telemetry.py
@@ -0,0 +1,34 @@
+import RNS
+
+class BasicTelemetryPlugin(SidebandTelemetryPlugin):
+ plugin_name = "telemetry_example"
+
+ def start(self):
+ # Do any initialisation work here
+ RNS.log("Basic telemetry plugin example starting...")
+
+ # And finally call start on superclass
+ super().start()
+
+ def stop(self):
+ # Do any teardown work here
+ pass
+
+ # And finally call stop on superclass
+ super().stop()
+
+ def update_telemetry(self, telemeter):
+ if telemeter != None:
+ RNS.log("Updating power sensors")
+ telemeter.synthesize("power_consumption")
+ telemeter.sensors["power_consumption"].update_consumer(2163.15, type_label="Heater consumption")
+ telemeter.sensors["power_consumption"].update_consumer(12.7/1e6, type_label="Receiver consumption")
+ telemeter.sensors["power_consumption"].update_consumer(0.055, type_label="LED consumption")
+ telemeter.sensors["power_consumption"].update_consumer(982.22*1e9, type_label="Smelter consumption")
+
+ telemeter.synthesize("power_production")
+ telemeter.sensors["power_production"].update_producer(5732.15, type_label="Solar production")
+
+# Finally, tell Sideband what class in this
+# file is the actual plugin class.
+plugin_class = BasicTelemetryPlugin
\ No newline at end of file
diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py
index 75653f18..0a81e9e2 100644
--- a/sbapp/sideband/core.py
+++ b/sbapp/sideband/core.py
@@ -17,7 +17,7 @@ import multiprocessing.connection
from threading import Lock
from .res import sideband_fb_data
from .sense import Telemeter, Commands
-from .plugins import SidebandCommandPlugin, SidebandServicePlugin
+from .plugins import SidebandCommandPlugin, SidebandServicePlugin, SidebandTelemetryPlugin
if RNS.vendor.platformutils.get_platform() == "android":
from jnius import autoclass, cast
@@ -247,6 +247,7 @@ class SidebandCore():
self.active_command_plugins = {}
self.active_service_plugins = {}
+ self.active_telemetry_plugins = {}
if self.is_service or self.is_standalone:
self.__load_plugins()
@@ -684,6 +685,7 @@ class SidebandCore():
plugin_globals = {}
plugin_globals["SidebandServicePlugin"] = SidebandServicePlugin
plugin_globals["SidebandCommandPlugin"] = SidebandCommandPlugin
+ plugin_globals["SidebandTelemetryPlugin"] = SidebandTelemetryPlugin
RNS.log("Loading plugin \""+str(file)+"\"", RNS.LOG_NOTICE)
plugin_path = os.path.join(plugins_path, file)
exec(open(plugin_path).read(), plugin_globals)
@@ -715,8 +717,27 @@ class SidebandCore():
pass
del plugin
+ elif issubclass(type(plugin), SidebandTelemetryPlugin):
+ plugin_name = plugin.plugin_name
+ if not plugin_name in self.active_telemetry_plugins:
+ self.active_telemetry_plugins[plugin_name] = plugin
+ RNS.log("Registered "+str(plugin)+" as telemetry plugin \""+str(plugin_name)+"\"", RNS.LOG_NOTICE)
+ else:
+ RNS.log("Could not register "+str(plugin)+" as telemetry plugin \""+str(plugin_name)+"\". Telemetry type was already registered", RNS.LOG_ERROR)
+ try:
+ plugin.stop()
+ except Exception as e:
+ pass
+ del plugin
+
else:
RNS.log("Unknown plugin type was loaded, ignoring it.", RNS.LOG_ERROR)
+ try:
+ plugin.stop()
+ except Exception as e:
+ pass
+ del plugin
+
else:
RNS.log("Plugin "+str(plugin)+" failed to start, ignoring it.", RNS.LOG_ERROR)
del plugin
@@ -2420,6 +2441,15 @@ class SidebandCore():
self.telemeter.disable(sensor)
else:
self.telemeter.disable(sensor)
+
+ for telemetry_plugin in self.active_telemetry_plugins:
+ try:
+ plugin = self.active_telemetry_plugins[telemetry_plugin]
+ plugin.update_telemetry(self.telemeter)
+
+ except Exception as e:
+ RNS.log("An error occurred while "+str(telemetry_plugin)+" was handling telemetry. The contained exception was: "+str(e), RNS.LOG_ERROR)
+ RNS.trace_exception(e)
if self.config["telemetry_s_fixed_location"]:
self.telemeter.synthesize("location")
diff --git a/sbapp/sideband/plugins.py b/sbapp/sideband/plugins.py
index 6775bc66..2de8ac42 100644
--- a/sbapp/sideband/plugins.py
+++ b/sbapp/sideband/plugins.py
@@ -39,3 +39,24 @@ class SidebandServicePlugin(SidebandPlugin):
def get_sideband(self):
return self.__sideband
+
+class SidebandTelemetryPlugin(SidebandPlugin):
+ def __init__(self, sideband_core):
+ self.__sideband = sideband_core
+ self.__started = False
+ self.plugin_name = type(self).plugin_name
+
+ def start(self):
+ self.__started = True
+
+ def stop(self):
+ self.__started = False
+
+ def is_running(self):
+ return self.__started == True
+
+ def get_sideband(self):
+ return self.__sideband
+
+ def update_telemetry(self, telemeter):
+ raise NotImplementedError
\ No newline at end of file
diff --git a/sbapp/sideband/sense.py b/sbapp/sideband/sense.py
index 471613c7..1eb8ba00 100644
--- a/sbapp/sideband/sense.py
+++ b/sbapp/sideband/sense.py
@@ -43,38 +43,28 @@ class Telemeter():
def __init__(self, from_packed=False, android_context=None, service=False, location_provider=None):
self.sids = {
- Sensor.SID_TIME: Time,
- Sensor.SID_RECEIVED: Received,
- Sensor.SID_INFORMATION: Information,
- Sensor.SID_BATTERY: Battery,
- Sensor.SID_PRESSURE: Pressure,
- Sensor.SID_LOCATION: Location,
- Sensor.SID_PHYSICAL_LINK: PhysicalLink,
- Sensor.SID_TEMPERATURE: Temperature,
- Sensor.SID_HUMIDITY: Humidity,
- Sensor.SID_MAGNETIC_FIELD: MagneticField,
- Sensor.SID_AMBIENT_LIGHT: AmbientLight,
- Sensor.SID_GRAVITY: Gravity,
- Sensor.SID_ANGULAR_VELOCITY: AngularVelocity,
- Sensor.SID_ACCELERATION: Acceleration,
- Sensor.SID_PROXIMITY: Proximity,
+ Sensor.SID_TIME: Time, Sensor.SID_RECEIVED: Received,
+ Sensor.SID_INFORMATION: Information, Sensor.SID_BATTERY: Battery,
+ Sensor.SID_PRESSURE: Pressure, Sensor.SID_LOCATION: Location,
+ Sensor.SID_PHYSICAL_LINK: PhysicalLink, Sensor.SID_TEMPERATURE: Temperature,
+ Sensor.SID_HUMIDITY: Humidity, Sensor.SID_MAGNETIC_FIELD: MagneticField,
+ Sensor.SID_AMBIENT_LIGHT: AmbientLight, Sensor.SID_GRAVITY: Gravity,
+ Sensor.SID_ANGULAR_VELOCITY: AngularVelocity, Sensor.SID_ACCELERATION: Acceleration,
+ Sensor.SID_PROXIMITY: Proximity, Sensor.SID_POWER_CONSUMPTION: PowerConsumption,
+ Sensor.SID_POWER_PRODUCTION: PowerProduction, Sensor.SID_PROCESSOR: Processor,
+ Sensor.SID_RAM: RandomAccessMemory, Sensor.SID_NVM: NonVolatileMemory,
}
self.available = {
"time": Sensor.SID_TIME,
- "information": Sensor.SID_INFORMATION,
- "received": Sensor.SID_RECEIVED,
- "battery": Sensor.SID_BATTERY,
- "pressure": Sensor.SID_PRESSURE,
- "location": Sensor.SID_LOCATION,
- "physical_link": Sensor.SID_PHYSICAL_LINK,
- "temperature": Sensor.SID_TEMPERATURE,
- "humidity": Sensor.SID_HUMIDITY,
- "magnetic_field": Sensor.SID_MAGNETIC_FIELD,
- "ambient_light": Sensor.SID_AMBIENT_LIGHT,
- "gravity": Sensor.SID_GRAVITY,
- "angular_velocity": Sensor.SID_ANGULAR_VELOCITY,
- "acceleration": Sensor.SID_ACCELERATION,
- "proximity": Sensor.SID_PROXIMITY,
+ "information": Sensor.SID_INFORMATION, "received": Sensor.SID_RECEIVED,
+ "battery": Sensor.SID_BATTERY, "pressure": Sensor.SID_PRESSURE,
+ "location": Sensor.SID_LOCATION, "physical_link": Sensor.SID_PHYSICAL_LINK,
+ "temperature": Sensor.SID_TEMPERATURE, "humidity": Sensor.SID_HUMIDITY,
+ "magnetic_field": Sensor.SID_MAGNETIC_FIELD, "ambient_light": Sensor.SID_AMBIENT_LIGHT,
+ "gravity": Sensor.SID_GRAVITY, "angular_velocity": Sensor.SID_ANGULAR_VELOCITY,
+ "acceleration": Sensor.SID_ACCELERATION, "proximity": Sensor.SID_PROXIMITY,
+ "power_consumption": Sensor.SID_POWER_CONSUMPTION, "power_production": Sensor.SID_POWER_PRODUCTION,
+ "processor": Sensor.SID_PROCESSOR, "ram": Sensor.SID_RAM, "nvm": Sensor.SID_NVM,
}
self.from_packed = from_packed
self.sensors = {}
@@ -180,22 +170,27 @@ class Telemeter():
class Sensor():
- SID_NONE = 0x00
- SID_TIME = 0x01
- SID_LOCATION = 0x02
- SID_PRESSURE = 0x03
- SID_BATTERY = 0x04
- SID_PHYSICAL_LINK = 0x05
- SID_ACCELERATION = 0x06
- SID_TEMPERATURE = 0x07
- SID_HUMIDITY = 0x08
- SID_MAGNETIC_FIELD = 0x09
- SID_AMBIENT_LIGHT = 0x0A
- SID_GRAVITY = 0x0B
- SID_ANGULAR_VELOCITY = 0x0C
- SID_PROXIMITY = 0x0E
- SID_INFORMATION = 0x0F
- SID_RECEIVED = 0x10
+ SID_NONE = 0x00
+ SID_TIME = 0x01
+ SID_LOCATION = 0x02
+ SID_PRESSURE = 0x03
+ SID_BATTERY = 0x04
+ SID_PHYSICAL_LINK = 0x05
+ SID_ACCELERATION = 0x06
+ SID_TEMPERATURE = 0x07
+ SID_HUMIDITY = 0x08
+ SID_MAGNETIC_FIELD = 0x09
+ SID_AMBIENT_LIGHT = 0x0A
+ SID_GRAVITY = 0x0B
+ SID_ANGULAR_VELOCITY = 0x0C
+ SID_PROXIMITY = 0x0E
+ SID_INFORMATION = 0x0F
+ SID_RECEIVED = 0x10
+ SID_POWER_CONSUMPTION = 0x11
+ SID_POWER_PRODUCTION = 0x12
+ SID_PROCESSOR = 0x13
+ SID_RAM = 0x14
+ SID_NVM = 0x15
def __init__(self, sid = None, stale_time = None):
self._telemeter = None
@@ -1311,4 +1306,176 @@ class Proximity(Sensor):
else:
return packed
except:
- return None
\ No newline at end of file
+ return None
+
+class PowerConsumption(Sensor):
+ SID = Sensor.SID_POWER_CONSUMPTION
+ STALE_TIME = 5
+
+ def __init__(self):
+ super().__init__(type(self).SID, type(self).STALE_TIME)
+
+ def setup_sensor(self):
+ self.update_data()
+
+ def teardown_sensor(self):
+ self.data = None
+
+ def update_consumer(self, power, type_label=None):
+ if type_label == None:
+ type_label = 0x00
+ elif type(type_label) != str:
+ return False
+
+ if self.data == None:
+ self.data = {}
+
+ self.data[type_label] = power
+ return True
+
+ def remove_consumer(self, type_label=None):
+ if type_label == None:
+ type_label = 0x00
+
+ if type_label in self.data:
+ self.data.pop(type_label)
+ return True
+
+ return False
+
+ def update_data(self):
+ pass
+
+ def pack(self):
+ d = self.data
+ if d == None:
+ return None
+ else:
+ packed = []
+ for type_label in self.data:
+ packed.append([type_label, self.data[type_label]])
+ return packed
+
+ def unpack(self, packed):
+ try:
+ if packed == None:
+ return None
+ else:
+ unpacked = {}
+ for entry in packed:
+ unpacked[entry[0]] = entry[1]
+ return unpacked
+
+ except:
+ return None
+
+ def render(self, relative_to=None):
+ if self.data == None:
+ return None
+
+ consumers = []
+ for type_label in self.data:
+ if type_label == 0x00:
+ label = "Power consumption"
+ else:
+ label = type_label
+ consumers.append({"label": label, "w": self.data[type_label]})
+
+ rendered = {
+ "icon": "power-plug-outline",
+ "name": "Power Consumption",
+ "values": consumers,
+ }
+
+ return rendered
+
+class PowerProduction(Sensor):
+ SID = Sensor.SID_POWER_PRODUCTION
+ STALE_TIME = 5
+
+ def __init__(self):
+ super().__init__(type(self).SID, type(self).STALE_TIME)
+
+ def setup_sensor(self):
+ self.update_data()
+
+ def teardown_sensor(self):
+ self.data = None
+
+ def update_producer(self, power, type_label=None):
+ if type_label == None:
+ type_label = 0x00
+ elif type(type_label) != str:
+ return False
+
+ if self.data == None:
+ self.data = {}
+
+ self.data[type_label] = power
+ return True
+
+ def remove_producer(self, type_label=None):
+ if type_label == None:
+ type_label = 0x00
+
+ if type_label in self.data:
+ self.data.pop(type_label)
+ return True
+
+ return False
+
+ def update_data(self):
+ pass
+
+ def pack(self):
+ d = self.data
+ if d == None:
+ return None
+ else:
+ packed = []
+ for type_label in self.data:
+ packed.append([type_label, self.data[type_label]])
+ return packed
+
+ def unpack(self, packed):
+ try:
+ if packed == None:
+ return None
+ else:
+ unpacked = {}
+ for entry in packed:
+ unpacked[entry[0]] = entry[1]
+ return unpacked
+
+ except:
+ return None
+
+ def render(self, relative_to=None):
+ if self.data == None:
+ return None
+
+ producers = []
+ for type_label in self.data:
+ if type_label == 0x00:
+ label = "Power Production"
+ else:
+ label = type_label
+ producers.append({"label": label, "w": self.data[type_label]})
+
+ rendered = {
+ "icon": "lightning-bolt",
+ "name": "Power Production",
+ "values": producers,
+ }
+
+ return rendered
+
+# TODO: Implement
+class Processor(Sensor):
+ pass
+
+class RandomAccessMemory(Sensor):
+ pass
+
+class NonVolatileMemory(Sensor):
+ pass
\ No newline at end of file
diff --git a/sbapp/ui/objectdetails.py b/sbapp/ui/objectdetails.py
index f208464d..b8e3be83 100644
--- a/sbapp/ui/objectdetails.py
+++ b/sbapp/ui/objectdetails.py
@@ -415,6 +415,78 @@ class RVDetails(MDRecycleView):
if q != None or rssi != None: snr_str = ", "+snr_str
if q_str or rssi_str or snr_str:
formatted_values = q_str+rssi_str+snr_str
+ elif name == "Power Consumption":
+ cs = s["values"]
+ if cs != None:
+ for c in cs:
+ label = c["label"]
+ watts = c["w"]
+ prefix = ""
+ if watts < 1/1e6:
+ watts *= 1e9
+ prefix = "n"
+ elif watts < 1/1e3:
+ watts *= 1e6
+ prefix = "µ"
+ elif watts < 1:
+ watts *= 1e3
+ prefix = "m"
+ elif watts >= 1e15:
+ watts /= 1e15
+ prefix = "E"
+ elif watts >= 1e12:
+ watts /= 1e12
+ prefix = "T"
+ elif watts >= 1e9:
+ watts /= 1e9
+ prefix = "G"
+ elif watts >= 1e6:
+ watts /= 1e6
+ prefix = "M"
+ elif watts >= 1e3:
+ watts /= 1e3
+ prefix = "K"
+
+ watts = round(watts, 2)
+ p_text = f"{label} [b]{watts} {prefix}W[/b]"
+ extra_entries.append({"icon": s["icon"], "text": p_text})
+
+ elif name == "Power Production":
+ cs = s["values"]
+ if cs != None:
+ for c in cs:
+ label = c["label"]
+ watts = c["w"]
+ prefix = ""
+ if watts < 1/1e6:
+ watts *= 1e9
+ prefix = "n"
+ elif watts < 1/1e3:
+ watts *= 1e6
+ prefix = "µ"
+ elif watts < 1:
+ watts *= 1e3
+ prefix = "m"
+ elif watts >= 1e15:
+ watts /= 1e15
+ prefix = "E"
+ elif watts >= 1e12:
+ watts /= 1e12
+ prefix = "T"
+ elif watts >= 1e9:
+ watts /= 1e9
+ prefix = "G"
+ elif watts >= 1e6:
+ watts /= 1e6
+ prefix = "M"
+ elif watts >= 1e3:
+ watts /= 1e3
+ prefix = "K"
+
+ watts = round(watts, 2)
+ p_text = f"{label} [b]{watts} {prefix}W[/b]"
+ extra_entries.append({"icon": s["icon"], "text": p_text})
+
elif name == "Location":
lat = s["values"]["latitude"]
lon = s["values"]["longitude"]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────